#include "zonesmanager.h"
#include <QDir>
#include <QDomDocument>
#include <QLibrary>
#include "QApplication.h"
#include <QSettings>
#include "proxyimpl.h"
#include <optrzone.h>
#include <optra/zoneinfo.h>
#include <QDomNodeList>
#include <QMapIterator>

#include "application.h"

#include <QtXml>
#include <QtXmlPatterns>

#include <QSqlDatabase>

#include <QMessageBox>

#include <optra/common/proxy.h>
#include <optra/common/db.h>
#include <optra/zoneinterface.h>

extern Optra::Proxy* proxy;
OptraZone * _ozone__Z_;
ZonesManager * zmanager;

const QString zoneDir = "zones/";

typedef bool (*Prototype)(Optra::ZoneInterface**);

bool readDatabaseData(QIODevice &device, QSettings::SettingsMap &map)
{
    return true;
}

bool writeDatabaseData(QIODevice &device, const QSettings::SettingsMap &map)
{
    return true;
}

OptraZone* ZonesManager::optra()
{
    return optrazone_;
}

ZonesManager::ZonesManager(Application* main, QObject *parent) :
        QObject(parent), main_(main)
{
    zmanager = this;

    optrazone_ = new OptraZone(main);
    setupZone(optrazone_, proxy->directory(Optra::Proxy::zones));
    _ozone__Z_ = optrazone_;



    QObject::connect(this, SIGNAL(zonesLoaded()), this, SLOT(dependency()));
    QObject::connect(this, SIGNAL(zonesXmlChecked()), this, SLOT(create()));
    QObject::connect(this, SIGNAL(zonesCreated()), this, SLOT(configure()));

    QObject::connect(this, SIGNAL(zonesConfigured()), this, SIGNAL(launchInitialize()));
    QObject::connect(this, SIGNAL(zonesConfigured()), this, SLOT(connect()));

    QObject::connect(this, SIGNAL(zonesConected()), this, SLOT(launch()));
    // QObject::connect(this, SIGNAL(zonesConected()), this, SIGNAL(launchLaunch()));



    dbFormat = QSettings::registerFormat("db", readDatabaseData, writeDatabaseData);
    QSettings::setDefaultFormat(dbFormat);
}



ZonesManager::~ZonesManager()
{
    foreach(QLibrary* lib, zones_libs){
        lib->unload();
        delete lib;
    }

    delete optrazone_;
}

bool ZonesManager::installZone(QFile* file)
{
    ZoneInternalInfo zi;
    if (!processZone(file, zi)){
        return false;
    }
    return installZone(zi);
}

void ZonesManager::updateZonesDependency(const QString& unique, int id)
{
    QSqlQuery query = optra()->db()->query();
    query.prepare("UPDATE dependency SET tid=:tid WHERE tid=0 AND tunique=:tuniq");
    query.bindValue(":tid", id);
    query.bindValue(":tuniq", unique);
    if (!query.exec()){
        qDebug() << "Update Zones Dependency filed";
    }
    query.prepare("UPDATE applicationsdep SET tid=:tid WHERE tid=0 AND tunique=:tuniq");

    if (!query.exec()){
        qDebug() << "Update Zones Dependency filed";
    }
}

bool ZonesManager::installZone(ZoneInternalInfo& zi)
{
    optra()->db()->db().transaction();
    int id;
    bool b = zoneInstaled(zi, &id);

    if (b && id != 0){
        zoneDrop(id);
    }
    QSqlQuery query = optra()->db()->query();
    query.prepare("INSERT INTO zones (path, displayname, guid, uni) VALUES (:path, :display, :guid, :unique)");
    query.bindValue(":path", zi.path);
    query.bindValue(":display", zi.desc);
    query.bindValue(":guid", QUuid::createUuid().toString());
    query.bindValue(":unique", zi.unique);
    if (!query.exec()){
        optra()->error("Install Zone Failed");
        optra()->db()->db().rollback();
        return false;
    }

    int lid = query.lastInsertId().toInt();

    zi.dbid = lid;

    query.exec(QString("DELETE FROM dependency WHERE sid=%1").arg(lid));
    if (!zi.critdep.isEmpty() && !setDBDependency(zi.critdep, lid, "Required")){
        if (QMessageBox::question(0, "Install Zone", "Dependency missing install anyway(zone will be disabled) ?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No){
            optra()->db()->db().rollback();
            optra()->warning("Install zone aborted");
            return false;
        }
    }

    if (!zi.funtdep.isEmpty() && !setDBDependency(zi.funtdep, lid, "Function")){
        optra()->info("Function zone(s) missing, some funcionality can be disabled, until you install required zone(s)");
    }

    if (!zi.futudep.isEmpty() && !setDBDependency(zi.futudep, lid, "Future")){
        optra()->info("Future zone(s) missing, extra funcionality can be disabled, until you install required zone(s)");
    }

    Optra::ZoneInterface* z = zoneInterface(zi.unique);

    if (z !=optrazone_ && !createZone(zi.path)){
        optra()->db()->db().rollback();
        optra()->warning("Install zone create error");
        return false;
    }



    if (!z || !z->install()){
        optra()->db()->db().rollback();
        optra()->warning("Install zone setup error");
        return false;
    }


    optra()->db()->db().commit();
    optra()->info("InstallZone ok");
    updateZonesDependency(zi.unique, lid);
    return true;
}

bool ZonesManager::setDBDependency(const QStringList& sl, int sid, const QString& depname)
{
    QSqlQuery query = optra()->db()->query();
    int typeidcri = zoneDependency(depname);

    bool ret = true;
    QVariantList tidl;
    QVariantList sidl;
    QVariantList typeId;
    QVariantList tuniq;

    foreach(QString dep, sl){
        sidl << sid;
        int id = 0;
        ZoneInternalInfo zi;
        zi.unique = dep;
        if (zoneInstaled(zi, &id)){
            tidl << id;
        }else{
            tidl << 0;
            ret = false;
        }
        tuniq << dep;
        typeId << typeidcri;
    }

    query.prepare("INSERT INTO dependency (sid, tid, typeid, tunique) VALUES (?, ?, ?, ?)");
    query.addBindValue(sidl);
    query.addBindValue(tidl);
    query.addBindValue(typeId);
    query.addBindValue(tuniq);

    if (!query.execBatch()){
        qDebug() << query.lastError();
        return false;
    }
    return ret;
}

int ZonesManager::zoneDependency(const QString& name)
{
    if (name.isEmpty()){
        return 0;
    }

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("dependencytype");
    model.setFilter(QString("displayname='%1'").arg(name));
    if (!model.select()){
        qFatal("zoneDependency select error");
        return false;
    }

    return model.record(0).value("id").toInt();
}

bool ZonesManager::zoneDrop(int id)
{
    if (id == 0){
        return false;
    }

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("zones");
    model.setFilter(QString("id=%1").arg(id));
    if (!model.select()){
        qFatal("zoneDrop select error");
        return false;
    }

    model.removeRow(0);
    model.database().transaction();
    if (!model.submitAll()){
        qDebug() << model.lastError().text();
        model.database().rollback();
        return false;
    }else{
        model.database().commit();
    }

    return true;
}

bool ZonesManager::recordZoneToInfo(QSqlRecord record, ZoneInternalInfo& info)
{
    info.dbid = record.value("id").toInt();
    info.unique = record.value("uni").toString();
    info.path = record.value("path").toString();
    info.displayName = record.value("displayname").toString();
    info.dir = proxy->directory(Optra::Proxy::zones);
    info.dir.cd(info.path);

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("dependency");

    int tid = zoneDependency("Required");
    model.setFilter(QString("sid=%1 AND typeid=%2").arg(info.dbid).arg(tid));
    model.select();
    for(int i = 0 ; i < model.rowCount(); i++){
        QString tu = model.record(i).value("tunique").toString();
        info.critdep << tu;
    }

    return true;
}

bool ZonesManager::zoneInfoUnique(const QString& unique, ZoneInternalInfo* info)
{
    if (unique.isEmpty())
        return false;

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("zones");
    model.setFilter(QString("uni='%1'").arg(unique));
    if (!model.select()){
        optra()->error("zoneInfoUnique select error");
        return false;
    }

    if (model.rowCount() < 1){
        return false;
    }

    if (info != NULL){
        recordZoneToInfo(model.record(0), *info);
    }

    return true;

}

bool ZonesManager::zoneInfoId(int id, ZoneInternalInfo& info)
{
    if (id == 0)
        return false;

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("zones");
    model.setFilter(QString("id=%1").arg(id));
    if (!model.select()){
        optra()->error("zoneInfoId select error");
        return false;
    }

    if (model.record().isEmpty()){
        return false;
    }
    recordZoneToInfo(model.record(0), info);

    return true;

}

bool ZonesManager::zoneInstaled(const ZoneInternalInfo& info, int* id)
{

    int idx = 0;
    bool ret = false;

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("zones");
    model.setFilter(QString("uni='%1'").arg(info.unique));

    if (!model.select()){
        qDebug() << model.lastError().text();
        qFatal("zoneInstaled select error");
        return false;
    }

    if (model.rowCount() > 0){
        idx = model.record(0).value("id").toInt();
        ret = true;
    }

    if (id){
        *id = idx;
    }
    return ret;
}

void ZonesManager::unloadZones()
{
    foreach(ZoneProcess pr, zoneProcess_.values()){
        qDebug() << "Unloading zone:" << pr.info->name().toAscii().data();
        pr.zinterface->unload();
        pr.zinterface->settings().sync();
        delete pr.thread;
    }

    zoneProcess_.clear();
    zones_.clear();
}

void ZonesManager::setupZone(Optra::ZoneInterface* zone, const QDir& working)
{

    zone->setProxy(proxy);
    zone->setBaseDir(working);
    if (zone->zoneInfo()->icon().isNull()){
        zone->zoneInfo()->setIcon(QIcon(":/icons/zone_default"));
    }

    QSettings* settings = new QSettings(dbFormat, QSettings::UserScope, "Optra Acme", "Optra System");
    settings->sync();

    zone->setSettings(settings);
    ZoneProcess process;
    process.info = zone->zoneInfo();
    process.zinterface = zone;
    process.thread = new ZoneThread(zone);

    zones_.append(zone);
    zoneProcess_[process.info->id()] = process;
}

bool ZonesManager::createZone(const QString& entryFile)
{
    qDebug() << "Loading File:" << entryFile;
    QDir zDir(qApp->applicationDirPath());
    zDir.cd(zoneDir);
    zDir.cd(entryFile);
    QLibrary *lib = new QLibrary(zDir.absoluteFilePath(entryFile));
    if (!lib->load()){
        errors << lib->errorString();
        qWarning() << "Can't open file:" << entryFile;
        delete lib;
        return false;
    }

    Prototype fn = (Prototype)lib->resolve("entry");
    Optra::ZoneInterface * zinfo = NULL;
    if (!fn){
        lib->unload();
        delete lib;
        errors << "no entry point";
        return false;
    }
    bool ret = false;
    try{
        ret = fn(&zinfo);
        QString v;
        v = zinfo->zoneInfo()->id();
        v = zinfo->zoneInfo()->name();

    }catch(...){
        lib->unload();
        delete lib;
        errors << "Zone Iterna Error";
        return false;
    }

    if (!ret){
        errors << "Initialization Error";
    }else{
        setupZone(zinfo, zDir);
        zones_libs.append(lib);
    }
    return ret;
}

bool ZonesManager::parseFolderElement(const QDomElement &element, ZoneInternalInfo& zi)
{
    if (element.isNull()){
        return false;
    }
    zi.path = element.text();
    return true;
}

bool ZonesManager::parseDependencyElement(const QDomElement &element, ZoneInternalInfo& zi)
{
    QString mode = element.attribute("mode", "");
    if (mode.isEmpty()){
        return false;
    }

    QDomElement e = element.firstChildElement("zones");
    if (e.isNull()){
        optra()->warning(QString("zone tag missing in dependency:%1 ").arg(mode));
        return false;
    }
    QStringList lst = e.text().split(",", QString::SkipEmptyParts);

    if (lst.isEmpty()){
        optra()->warning(QString("zone tag empty in dependency:%1 ").arg(mode));
        return false;
    }

    if (mode.compare("required") == 0){
        zi.critdep = lst;
    }else if (mode.compare("function") == 0){
        zi.funtdep = lst;
    }else if (mode.compare("future") == 0){
        zi.futudep = lst;
    }
    return true;
}

bool ZonesManager::processZone(QFile* xmlFile, ZoneInternalInfo& zi)
{
    optra()->info(QString("Process Zone:%1").arg(xmlFile->fileName()));

    QFile schema(":/schemas/zone_schema");
    schema.open(QFile::ReadOnly);

    QXmlSchema sh;
    if (!sh.load(&schema)){
        optra()->error("Unable to load XMLSchema file");
        return false;
    }

    QXmlSchemaValidator validator(sh);
    if (!validator.validate(xmlFile)){
        optra()->warning(QString("Zone Schema error:%1").arg(xmlFile->fileName()));
        return false;
    }else{
        optra()->debug("zone xml file schema validated");
    }

    QDomDocument doc("zones");
    QString errm;
    int errl;
    int errc;
    xmlFile->reset();
    if (!doc.setContent(xmlFile, &errm, &errl, &errc)) {
        errors << "Parse error:" + xmlFile->fileName();
        optra()->warning(QString("Zone XML load error:%1 (line:%2, column:%3, message:%4)").arg(xmlFile->fileName()).arg(errl).arg(errc).arg(errm));
        return false;
    }

    QDomElement docElem = doc.documentElement();
    if (docElem.isNull()){
        optra()->warning(QString("Missing root tag in:%1").arg(xmlFile->fileName()));
        return false;
    }

    QDomElement e = docElem.firstChildElement("path");
    if (!parseFolderElement(e, zi)){
        optra()->warning(QString("Missing path tag in:%1").arg(xmlFile->fileName()));
        return false;
    }

    e = docElem.firstChildElement("dependency");
    while (!e.isNull()){
        if (!parseDependencyElement(e, zi)){
            optra()->warning(QString("Wrong dependency tag in:%1").arg(xmlFile->fileName()));
            return false;
        }
        e = e.nextSiblingElement("dependency");
    }

    e = docElem.firstChildElement("unique");
    if (e.isNull()){
        optra()->warning(QString("Missing unique tag in:%1 ").arg(xmlFile->fileName()));
        return false;
    }else{
        zi.unique = e.text();
    }

    e = docElem.firstChildElement("name");
    if (!e.isNull()){
        zi.desc = e.text();
    }else{
        zi.desc = zi.unique;
    }

    return true;
}

void ZonesManager::create()
{
    optra()->debug("Creating zones");
    QMapIterator<QString, ZoneInternalInfo> ir(zoneinfo_);
    while(ir.hasNext()){
        ir.next();
        ZoneInternalInfo i = ir.value();
        i.dir.cd(i.path);
        bool b = createZone(i.dir.absoluteFilePath(i.path));
    }

    emit zonesCreated();
}

void ZonesManager::dependency()
{
    optra()->debug("Checking zones dependency");
    QMapIterator<QString, ZoneInternalInfo> ir(zoneinfo_);
    while(ir.hasNext()){
        ir.next();
        QStringList crit = ir.value().critdep;
        foreach(QString s, crit){
            if (!zoneInfoUnique(s)){
                optra()->warning(QString("Required by:%1 zone:%2 missing, zone is skipinng").arg(ir.value().unique).arg(s));
                zoneinfo_.remove(ir.value().unique);
                break;
            }
        }
    }
    emit zonesXmlChecked();
}

bool ZonesManager::loadZones()
{
    errors.clear();


    qDebug("Loading Zones...");
    QDir zDir(qApp->applicationDirPath());
    zDir.cd(zoneDir);

    setupZone(optrazone_, zDir);

    ZoneInternalInfo zi;

    Optra::ZoneInterface* zinter = static_cast<Optra::ZoneInterface*>(optrazone_);

    zi.unique = zinter->zoneInfo()->id();
    zi.desc = zinter->zoneInfo()->name();
    if (!zoneInfoUnique(zi.unique)){
        zinter->install();
        installZone(zi);
    }

    QSqlTableModel model(this, optra()->db()->db());
    model.setTable("zones");
    model.setFilter(QString("uni<>'%1'").arg(zi.unique));
    model.select();

    for (int i = 0; i < model.rowCount(); i++){
        QSqlRecord r =  model.record(i);
        ZoneInternalInfo zi;
        recordZoneToInfo(r, zi);
        zoneinfo_.insert(zi.unique, zi);
    }

    qDebug() << "Loading done.";
    emit zonesLoaded();
    return true;
}

void ZonesManager::configure()
{
    optra()->debug("Configure zones");
    foreach(ZoneProcess z, zoneProcess_.values()){
        // process.thread->start();
        //   main_->connectToZone(z.info->id(),  SLOT(Optra::ZoneInterface::initialize()), this, SIGNAL(launchInitialize()));
        //  main_->connectToZone(z.info->id(),  SLOT(Optra::ZoneInterface::launch()), this, SIGNAL(launchLaunch()));
        z.zinterface->initialize();
    }
    qDebug() << "Configure done.";
    emit zonesConfigured();
}

void ZonesManager::connect()
{
    //    qDebug() << "Connecting zones...";
    //    foreach(Optra::ZoneInterface* z, zones_){
    //        z->connectZones();
    //
    //    }
    //    qDebug() << "Connecting done.";
    emit zonesConected();
}

void ZonesManager::launch()
{
    optra()->info("Launching zones");
    foreach(ZoneProcess z, zoneProcess_.values()){


        // process.thread->start();
        z.zinterface->launch();
    }
    qDebug() << "launch done.";
    emit zonesLaunched();
}

Optra::ZoneInterface* ZonesManager::zoneInterface(const QString& name)
{
    if (!zoneProcess_.contains(name))
        return NULL;

    return zoneProcess_[name].zinterface;
}
